// Copyright 2014 PDFium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com

#include "../../public/fpdf_ppo.h"
#include "../../third_party/base/nonstd_unique_ptr.h"
#include "../include/fsdk_define.h"

class CPDF_PageOrganizer
{
public:
    using ObjectNumberMap = std::map<FX_DWORD, FX_DWORD>;
    CPDF_PageOrganizer();
    ~CPDF_PageOrganizer();

    FX_BOOL PDFDocInit(CPDF_Document* pDestPDFDoc, CPDF_Document* pSrcPDFDoc);
    FX_BOOL ExportPage(CPDF_Document* pSrcPDFDoc,
                       CFX_WordArray* nPageNum,
                       CPDF_Document* pDestPDFDoc,
                       int nIndex);
    CPDF_Object* PageDictGetInheritableTag(CPDF_Dictionary* pDict,
                                           CFX_ByteString nSrctag);
    FX_BOOL UpdateReference(CPDF_Object* pObj,
                            CPDF_Document* pDoc,
                            ObjectNumberMap* pObjNumberMap);
    FX_DWORD GetNewObjId(CPDF_Document* pDoc,
                         ObjectNumberMap* pObjNumberMap,
                         CPDF_Reference* pRef);
};


CPDF_PageOrganizer::CPDF_PageOrganizer()
{
}

CPDF_PageOrganizer::~CPDF_PageOrganizer()
{
}

FX_BOOL CPDF_PageOrganizer::PDFDocInit(CPDF_Document* pDestPDFDoc,
                                       CPDF_Document* pSrcPDFDoc)
{
    if (!pDestPDFDoc || !pSrcPDFDoc)
        return FALSE;

    CPDF_Dictionary* pNewRoot = pDestPDFDoc->GetRoot();
    if (!pNewRoot)
        return FALSE;

    //Set the document information////////////////////////////////////////////

    CPDF_Dictionary* DInfoDict = pDestPDFDoc->GetInfo();
    if (!DInfoDict)
        return FALSE;

    CFX_ByteString producerstr;
    producerstr.Format("PDFium");
    DInfoDict->SetAt("Producer", new CPDF_String(producerstr));

    //Set type////////////////////////////////////////////////////////////////
    CFX_ByteString cbRootType = pNewRoot->GetString("Type", "");
    if (cbRootType.Equal("") ) {
        pNewRoot->SetAt("Type", new CPDF_Name("Catalog"));
    }

    CPDF_Object* pElement = pNewRoot->GetElement("Pages");
    CPDF_Dictionary* pNewPages = pElement ?
        (CPDF_Dictionary*)pElement->GetDirect() : nullptr;
    if (!pNewPages) {
        pNewPages = new CPDF_Dictionary;
        FX_DWORD NewPagesON = pDestPDFDoc->AddIndirectObject(pNewPages);
        pNewRoot->SetAt("Pages", new CPDF_Reference(pDestPDFDoc, NewPagesON));
    }

    CFX_ByteString cbPageType = pNewPages->GetString("Type", "");
    if (cbPageType.Equal("")) {
        pNewPages->SetAt("Type", new CPDF_Name("Pages"));
    }

    CPDF_Array* pKeysArray = pNewPages->GetArray("Kids");
    if (!pKeysArray) {
        CPDF_Array* pNewKids = new CPDF_Array;
        FX_DWORD Kidsobjnum = -1;
        Kidsobjnum = pDestPDFDoc->AddIndirectObject(pNewKids);

        pNewPages->SetAt("Kids", new CPDF_Reference(pDestPDFDoc, Kidsobjnum));
        pNewPages->SetAt("Count", new CPDF_Number(0));
    }

    return TRUE;
}

FX_BOOL CPDF_PageOrganizer::ExportPage(CPDF_Document* pSrcPDFDoc,
                                       CFX_WordArray* nPageNum,
                                       CPDF_Document* pDestPDFDoc,
                                       int nIndex)
{
    int curpage = nIndex;

    nonstd::unique_ptr<ObjectNumberMap> pObjNumberMap(new ObjectNumberMap);

    for (int i = 0; i < nPageNum->GetSize(); ++i) {
        CPDF_Dictionary* pCurPageDict = pDestPDFDoc->CreateNewPage(curpage);
        CPDF_Dictionary* pSrcPageDict =
            pSrcPDFDoc->GetPage(nPageNum->GetAt(i) - 1);
        if (!pSrcPageDict || !pCurPageDict)
            return FALSE;

        // Clone the page dictionary///////////
        FX_POSITION SrcPos = pSrcPageDict->GetStartPos();
        while (SrcPos) {
            CFX_ByteString cbSrcKeyStr;
            CPDF_Object* pObj = pSrcPageDict->GetNextElement(SrcPos,
                                                             cbSrcKeyStr);
            if (cbSrcKeyStr.Compare(("Type")) &&
                cbSrcKeyStr.Compare(("Parent"))) {
                if (pCurPageDict->KeyExist(cbSrcKeyStr))
                    pCurPageDict->RemoveAt(cbSrcKeyStr);
                pCurPageDict->SetAt(cbSrcKeyStr, pObj->Clone());
            }
        }

        //inheritable item///////////////////////
        CPDF_Object* pInheritable = nullptr;
        //1 MediaBox  //required
        if (!pCurPageDict->KeyExist("MediaBox")) {
            pInheritable = PageDictGetInheritableTag(pSrcPageDict, "MediaBox");
            if (!pInheritable) {
                // Search the "CropBox" from source page dictionary,
                // if not exists,we take the letter size.
                pInheritable = PageDictGetInheritableTag(pSrcPageDict,
                                                         "CropBox");
                if (pInheritable) {
                    pCurPageDict->SetAt("MediaBox", pInheritable->Clone());
                } else {
                    // Make the default size to be letter size (8.5'x11')
                    CPDF_Array* pArray = new CPDF_Array;
                    pArray->AddNumber(0);
                    pArray->AddNumber(0);
                    pArray->AddNumber(612);
                    pArray->AddNumber(792);
                    pCurPageDict->SetAt("MediaBox", pArray);
                }
            } else {
                pCurPageDict->SetAt("MediaBox", pInheritable->Clone());
            }
        }
        //2 Resources //required
        if (!pCurPageDict->KeyExist("Resources")) {
            pInheritable = PageDictGetInheritableTag(pSrcPageDict, "Resources");
            if (!pInheritable)
                return FALSE;
            pCurPageDict->SetAt("Resources", pInheritable->Clone());
        }
        //3 CropBox  //Optional
        if (!pCurPageDict->KeyExist("CropBox")) {
            pInheritable = PageDictGetInheritableTag(pSrcPageDict, "CropBox");
            if (pInheritable)
                pCurPageDict->SetAt("CropBox", pInheritable->Clone());
        }
        //4 Rotate  //Optional
        if (!pCurPageDict->KeyExist("Rotate")) {
            pInheritable = PageDictGetInheritableTag(pSrcPageDict, "Rotate");
            if (pInheritable)
                pCurPageDict->SetAt("Rotate", pInheritable->Clone());
        }

        /////////////////////////////////////////////
        //Update the reference
        FX_DWORD dwOldPageObj = pSrcPageDict->GetObjNum();
        FX_DWORD dwNewPageObj = pCurPageDict->GetObjNum();

        (*pObjNumberMap)[dwOldPageObj] = dwNewPageObj;

        UpdateReference(pCurPageDict, pDestPDFDoc, pObjNumberMap.get());
        ++curpage;
    }

    return TRUE;
}

CPDF_Object* CPDF_PageOrganizer::PageDictGetInheritableTag(
    CPDF_Dictionary* pDict,
    CFX_ByteString nSrctag)
{
    if (!pDict || nSrctag.IsEmpty())
        return nullptr;
    if (!pDict->KeyExist("Parent") || !pDict->KeyExist("Type"))
        return nullptr;

    CPDF_Object* pType = pDict->GetElement("Type")->GetDirect();
    if (!pType || pType->GetType() != PDFOBJ_NAME)
        return nullptr;
    if (pType->GetString().Compare("Page"))
        return nullptr;

    CPDF_Object* pParent = pDict->GetElement("Parent")->GetDirect();
    if (!pParent || pParent->GetType() != PDFOBJ_DICTIONARY)
        return nullptr;

    if (pDict->KeyExist((const char*)nSrctag))
        return pDict->GetElement((const char*)nSrctag);

    CPDF_Dictionary* pp = (CPDF_Dictionary*)pParent;
    while (pp) {
        if (pp->KeyExist((const char*)nSrctag)) {
            return pp->GetElement((const char*)nSrctag);
        }
        if (!pp->KeyExist("Parent")) {
            break;
        }
        pp = (CPDF_Dictionary*)pp->GetElement("Parent")->GetDirect();
        if (pp->GetType() == PDFOBJ_NULL) {
            break;
        }
    }

    return nullptr;
}

FX_BOOL CPDF_PageOrganizer::UpdateReference(CPDF_Object* pObj,
                                            CPDF_Document* pDoc,
                                            ObjectNumberMap* pObjNumberMap)
{
    switch (pObj->GetType()) {
        case PDFOBJ_REFERENCE: {
            CPDF_Reference* pReference = (CPDF_Reference*)pObj;
            FX_DWORD newobjnum = GetNewObjId(pDoc, pObjNumberMap, pReference);
            if (newobjnum == 0)
                return FALSE;
            pReference->SetRef(pDoc, newobjnum);
            break;
        }
        case PDFOBJ_DICTIONARY: {
            CPDF_Dictionary* pDict = (CPDF_Dictionary*)pObj;

            FX_POSITION pos = pDict->GetStartPos();
            while (pos) {
                CFX_ByteString key("");
                CPDF_Object* pNextObj = pDict->GetNextElement(pos, key);
                if (!FXSYS_strcmp(key, "Parent") ||
                    !FXSYS_strcmp(key, "Prev") ||
                    !FXSYS_strcmp(key, "First")) {
                    continue;
                }
                if (pNextObj) {
                    if (!UpdateReference(pNextObj, pDoc, pObjNumberMap))
                      pDict->RemoveAt(key);
                } else {
                    return FALSE;
                }
          }
          break;
        }
        case PDFOBJ_ARRAY: {
            CPDF_Array* pArray = (CPDF_Array*)pObj;
            FX_DWORD count = pArray->GetCount();
            for (FX_DWORD i = 0; i < count; ++i) {
                CPDF_Object* pNextObj = pArray->GetElement(i);
                if (!pNextObj)
                    return FALSE;
                if (!UpdateReference(pNextObj, pDoc, pObjNumberMap))
                    return FALSE;
            }
            break;
        }
        case PDFOBJ_STREAM: {
            CPDF_Stream* pStream = (CPDF_Stream*)pObj;
            CPDF_Dictionary* pDict = pStream->GetDict();
            if (pDict) {
                if (!UpdateReference(pDict, pDoc, pObjNumberMap))
                    return FALSE;
            } else {
                return FALSE;
            }
            break;
        }
        default:
            break;
    }

    return TRUE;
}

FX_DWORD CPDF_PageOrganizer::GetNewObjId(CPDF_Document* pDoc,
                                         ObjectNumberMap* pObjNumberMap,
                                         CPDF_Reference* pRef)
{
    if (!pRef)
        return 0;

    FX_DWORD dwObjnum = pRef->GetRefObjNum();
    FX_DWORD dwNewObjNum = 0;
    const auto it = pObjNumberMap->find(dwObjnum);
    if (it != pObjNumberMap->end())
        dwNewObjNum = it->second;
    if (dwNewObjNum)
        return dwNewObjNum;

    CPDF_Object* pDirect = pRef->GetDirect();
    if (!pDirect)
        return 0;

    CPDF_Object* pClone = pDirect->Clone();
    if (!pClone)
        return 0;

    if (pClone->GetType() == PDFOBJ_DICTIONARY) {
        CPDF_Dictionary* pDictClone = (CPDF_Dictionary*)pClone;
        if (pDictClone->KeyExist("Type")) {
            CFX_ByteString strType = pDictClone->GetString("Type");
            if (!FXSYS_stricmp(strType, "Pages")) {
                pDictClone->Release();
                return 4;
            }
            if (!FXSYS_stricmp(strType, "Page")) {
                pDictClone->Release();
                return  0;
            }
        }
    }
    dwNewObjNum = pDoc->AddIndirectObject(pClone);
    (*pObjNumberMap)[dwObjnum] = dwNewObjNum;

    if (!UpdateReference(pClone, pDoc, pObjNumberMap)) {
        pClone->Release();
        return 0;
    }
    return dwNewObjNum;
}

FPDF_BOOL ParserPageRangeString(CFX_ByteString rangstring,
                                CFX_WordArray* pageArray,
                                int nCount)
{
    if (rangstring.GetLength() != 0) {
        rangstring.Remove(' ');
        int nLength = rangstring.GetLength();
        CFX_ByteString cbCompareString("0123456789-,");
        for (int i = 0; i < nLength; ++i) {
            if (cbCompareString.Find(rangstring[i]) == -1)
                return FALSE;
        }
        CFX_ByteString cbMidRange;
        int nStringFrom = 0;
        int nStringTo = 0;
        while (nStringTo < nLength) {
            nStringTo = rangstring.Find(',', nStringFrom);
            if (nStringTo == -1)
                nStringTo = nLength;
            cbMidRange = rangstring.Mid(nStringFrom, nStringTo - nStringFrom);
            int nMid = cbMidRange.Find('-');
            if (nMid == -1) {
                long lPageNum = atol(cbMidRange);
                if (lPageNum <= 0 || lPageNum > nCount)
                    return FALSE;
                pageArray->Add((FX_WORD)lPageNum);
            } else {
                int nStartPageNum = atol(cbMidRange.Mid(0, nMid));
                if (nStartPageNum == 0)
                    return FALSE;

                ++nMid;
                int nEnd = cbMidRange.GetLength() - nMid;
                if (nEnd == 0)
                    return FALSE;

                int nEndPageNum = atol(cbMidRange.Mid(nMid, nEnd));
                if (nStartPageNum < 0 ||
                    nStartPageNum >nEndPageNum ||
                    nEndPageNum > nCount) {
                    return FALSE;
                }
                for (int i = nStartPageNum; i <= nEndPageNum; ++i) {
                    pageArray->Add(i);
                }
            }
            nStringFrom = nStringTo + 1;
        }
    }
    return TRUE;
}

DLLEXPORT FPDF_BOOL STDCALL FPDF_ImportPages(FPDF_DOCUMENT dest_doc,
                                             FPDF_DOCUMENT src_doc,
                                             FPDF_BYTESTRING pagerange,
                                             int index)
{
    if (!dest_doc || !src_doc)
        return FALSE;

    CFX_WordArray pageArray;
    CPDF_Document* pSrcDoc = (CPDF_Document*)src_doc;
    int nCount = pSrcDoc->GetPageCount();
    if (pagerange) {
        if (!ParserPageRangeString(pagerange,&pageArray,nCount))
            return FALSE;
    } else {
        for (int i = 1; i <= nCount; ++i) {
            pageArray.Add(i);
        }
    }

    CPDF_Document* pDestDoc = (CPDF_Document*)dest_doc;
    CPDF_PageOrganizer pageOrg;

    pageOrg.PDFDocInit(pDestDoc, pSrcDoc);

    return pageOrg.ExportPage(pSrcDoc,&pageArray,pDestDoc,index);
}

DLLEXPORT FPDF_BOOL STDCALL FPDF_CopyViewerPreferences(FPDF_DOCUMENT dest_doc,
                                                       FPDF_DOCUMENT src_doc)
{
    if (!src_doc || !dest_doc)
        return false;

    CPDF_Document* pSrcDoc = (CPDF_Document*)src_doc;
    CPDF_Dictionary* pSrcDict = pSrcDoc->GetRoot();
    pSrcDict = pSrcDict->GetDict(FX_BSTRC("ViewerPreferences"));;
    if (!pSrcDict)
        return FALSE;

    CPDF_Document* pDstDoc = (CPDF_Document*)dest_doc;
    CPDF_Dictionary* pDstDict = pDstDoc->GetRoot();
    if (!pDstDict)
        return FALSE;
    pDstDict->SetAt(FX_BSTRC("ViewerPreferences"), pSrcDict->Clone(TRUE));
    return TRUE;
}
